TypeScript で記述した Google Apps Script を clasp と GitHub Actions を使ってデプロイする
@google/clasp を使うことで CLI で Google Apps Script (GAS) を扱えるため、コードを Git で管理できるようになります。
今回はコードを GitHub で管理し、テストと clasp push
を Github Actions で実行できるようにしてみます。
最終的な完成物は下記のリポジトリになります。
(環境変数を設定するところを間違えてハマってしまったため、変なタグがいっぱいあります……)
インストール
インストールは公式の手順に従ってください。
$ npm install -g @google/clasp $ exec $SHELL -l $ clasp -v 2.3.0
Google Apps Script API が有効でなければ有効化しておく必要があります。おそらく初期では無効になっているかと思います。
有効になっていない場合、以下のようなエラーが表示されます。
GaxiosError: User has not enabled the Apps Script API. Enable it by visiting https://script.google.com/home/usersettings then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.
プロジェクトを作成する
プロジェクトの作成は clasp create
で行います。
$ mkdir clasp-github-actions-example && cd $_ $ clasp create --type standalone Could not read API credentials. Are you logged in globally?
初めて作成する場合には、credential となる .clasprc.json
が作成されていないため、上記のようにエラーとなります。
その場合には clasp login
でログインし、~/.clasprc.json
が作成します。
Default credentials saved to: ~/.clasprc.json (/Users/YOUR_NAME/.clasprc.json).
初めてのログイン時には上記のように権限の許可が必要になります。
あらためて clasp create
してみましょう。
$ clasp create --type standalone Created new standalone script: https://script.google.com/d/xxxxxxxx/edit Warning: files in subfolder are not accounted for unless you set a '.claspignore' file. Cloned 1 file. └─ appsscript.json
TypeScript 対応
clasp は v1.5.0 以降、 TypeScript のままでも自動で変換してくれるようになりました。
まずは型定義を追加します。
yarn add -D @types/google-apps-script
次に tsconfig.json を追加しましょう。下記の設定は公式ドキュメントの記載されているものに、"strict": true
を追加したものです。
// tsconfig.json { "compilerOptions": { "lib": ["esnext"], "experimentalDecorators": true, "strict": true } }
実際にデプロイするコードは、ここでは公式ドキュメントのものをそのまま使用していきます。
// src/hello.ts const greeter = (person: string) => { return `Hello, ${person}!`; }; function testGreeter() { const user = "Grant"; Logger.log(greeter(user)); }
ESLint と Prettier の導入
ESLint と Prettier も導入しておきましょう。
yarn add -D eslint prettier @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier eslint-plugin-import
ここでは Prettier はデフォルトの設定を使用するため、設定ファイルは追加しません。.eslint.js
のみ追加しています。
.eslint.js
module.exports = { extends: ["eslint:recommended"], env: { browser: true, node: true, es6: true, }, parserOptions: { ecmaVersion: 2020, }, overrides: [ { files: ["**/*.ts"], extends: [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier", ], parser: "@typescript-eslint/parser", parserOptions: { sourceType: "module", project: "./tsconfig.json", }, plugins: ["@typescript-eslint", "import"], rules: { "no-unused-vars": "off", "import/order": "error", "@typescript-eslint/no-unused-vars": [ "error", { varsIgnorePattern: "testGreeter" }, ], }, }, ], };
エントリポイントとなる関数に no-unused-vars のエラーが表示されるため、varsIgnorePattern
に関数名の追加が必要になります。
テストの追加
次にテストのための jest 導入と、実際にテストを一つ追加していきます。
yarn add -D @types/jest jest ts-jest
jest.config.js
を追加します。
// jest.config.js module.exports = { roots: ["<rootDir>/src"], testMatch: ["**/__tests__/**/*.+(ts|js)", "**/?(*.)+(spec|test).+(ts|js)"], transform: { "^.+\\.ts$": "ts-jest", }, };
簡単なテストを用意しておきます。
// src/hello.test.ts import { greeter } from "./hello"; describe(greeter.name, () => { it("should return greeting", () => { const greeting = greeter("John"); expect(greeting).toBe("Hello, John!"); }); });
GitHub Actions
Lint と Test
main ブランチにプルリクエストがでたときに、Lint と Test が実行されるようにしておきます。
.github/workflows/test.yml
name: Test on: pull_request: types: [opened, synchronize, reopened] branches: - main jobs: lint: name: Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2-beta with: node-version: "14" - id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - uses: actions/cache@v2 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- - run: yarn install --frozen-lockfile - run: yarn lint test: name: Test runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2-beta with: node-version: "14" - id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - uses: actions/cache@v2 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- - run: yarn install --frozen-lockfile - run: yarn test:ci
typecheck をしていない理由
上記のエラーのスマートな解決方法がわからなかったため、ここでは typecheck を実行していません。
デプロイ
ここでは git の tag を利用してデプロイし、そのタグを npx @google/clasp version
することでバージョニングをしていきます。
バージョニングをしておくと、次のように GAS のエディタ上でデプロイの管理から過去のバージョンの自動生成されたドキュメントを確認できるようになります。
.github/workflows/release.yml
name: Publish Release on: push: tags: - "v*" jobs: release: runs-on: ubuntu-latest env: CLASPRC_ACCESS_TOKEN: ${{ secrets.CLASPRC_ACCESS_TOKEN }} CLASPRC_CLIENT_ID: ${{ secrets.CLASPRC_CLIENT_ID }} CLASPRC_CLIENT_SECRET: ${{ secrets.CLASPRC_CLIENT_SECRET }} CLASPRC_EXPIRY_DATE: ${{ secrets.CLASPRC_EXPIRY_DATE }} CLASPRC_ID_TOKEN: ${{ secrets.CLASPRC_ID_TOKEN }} CLASPRC_REFRESH_TOKEN: ${{ secrets.CLASPRC_REFRESH_TOKEN }} CLASP_SCRIPT_ID: ${{ secrets.CLASP_SCRIPT_ID }} steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2-beta with: node-version: "14" - name: Cache node modules uses: actions/cache@v2 env: cache-name: cache-node-modules with: path: ~/.npm key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-build-${{ env.cache-name }}- ${{ runner.os }}-build- ${{ runner.os }}- - name: Create ~/.clasprc.json run: | echo $(cat <<-EOS { "token": { "access_token": "${CLASPRC_ACCESS_TOKEN}", "scope": "https://www.googleapis.com/auth/script.deployments https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/drive.file openid https://www.googleapis.com/auth/service.management https://www.googleapis.com/auth/script.projects https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/drive.metadata.readonly https://www.googleapis.com/auth/logging.read https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/script.webapp.deploy", "token_type": "Bearer", "id_token": "${CLASPRC_ID_TOKEN}", "expiry_date": ${CLASPRC_EXPIRY_DATE}, "refresh_token": "${CLASPRC_REFRESH_TOKEN}" }, "oauth2ClientSettings": { "clientId": "${CLASPRC_CLIENT_ID}", "clientSecret": "${CLASPRC_CLIENT_SECRET}", "redirectUri": "http://localhost" }, "isLocalCreds": false } EOS ) > ~/.clasprc.json - name: Create ~/.clasp.json run: | echo $(cat <<-EOS { "scriptId": "${CLASP_SCRIPT_ID}" } EOS ) > ./.clasp.json - name: Get version id: get_version run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/} - name: Upload files run: npx @google/clasp push --force - name: Add version run: npx @google/clasp version ${{ steps.get_version.outputs.VERSION }}
いくつか GitHub に環境変数を設定しておく必要があります。
デプロイは git tag v1.0.0
のようにタグを付けた後に、git push origin v1.0.0
としてタグを push するだけです。clasp open
でブラウザ上で Apps Script のエディタを開くことができるので、デプロイされているか確認してみましょう。
secrets には JSON をそのまま値として入れておけばいい?
GitHub がログのシークレットを確実に削除するよう、シークレットの値として構造化データを使用しないでください。 たとえば、JSONやエンコードされたGit blobを含むシークレットは作成しないでください。
.clasprc.json
や .clasp.json
を JSON のまま secrets に入れるようなことはは、公式ドキュメントで作成しないでくださいと書かれているので、それぞれ一つずつ設定し、JSON の生成を GitHub Actions で行いました。
ためしにトリガーで実行してみる
エディタ上で確認することもできますが、ためしにトリガーを使用して testGreeter
を定期実行してみます。
左のメニューバーにあるトリガーから、新規のトリガーを追加します。
ここでは一分ごとに定期実行させてみました。数分待った後に、実行数を確認してみましょう。
実行できていそうです。
トリガーの実行時間は、無料枠で執筆時点で 90 min / day ですので、時間のかかる処理を定期実行する場合には注意が必要です。その他にも API の実行回数もありますので、実際に使用する場合には下記のページで確認しておきましょう。
まとめ
以前はトランスパイルが必要だった clasp も、今では TypeScript のままデプロイが可能になりました。さらに GitHub Actions を使って簡単にテストの実行、デプロイが可能です。GAS のトリガーを使えば無料枠内でも定期的にいろんなことを実行することもできます。